package org.robolectric.annotation.processing.generator;

import com.google.common.base.Joiner;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import org.robolectric.annotation.processing.RobolectricModel;
import org.robolectric.annotation.processing.RobolectricModel.ShadowInfo;
import org.robolectric.annotation.processing.RobolectricProcessor;

/**
 * Generator that creates the "ShadowProvider" implementation for a shadow package.
 */
public class ShadowProviderGenerator extends Generator {
  private final Filer filer;
  private final Messager messager;
  private final RobolectricModel model;
  private final String shadowPackage;
  private final boolean shouldInstrumentPackages;
  private final int priority;

  public ShadowProviderGenerator(
      RobolectricModel model,
      ProcessingEnvironment environment,
      String shadowPackage,
      boolean shouldInstrumentPackages,
      int priority) {
    this.messager = environment.getMessager();
    this.filer = environment.getFiler();
    this.model = model;
    this.shadowPackage = shadowPackage;
    this.shouldInstrumentPackages = shouldInstrumentPackages;
    this.priority = priority;
  }

  @Override
  public void generate() {
    if (shadowPackage == null) {
      return;
    }

    final String shadowClassName = shadowPackage + '.' + GEN_CLASS;

    // TODO: Because this was fairly simple to begin with I haven't
    // included a templating engine like Velocity but simply used
    // raw print() statements, in an effort to reduce the number of
    // dependencies that RAP has. However, if it gets too complicated
    // then using Velocity might be a good idea.
    try {
      JavaFileObject jfo = filer.createSourceFile(shadowClassName);
      try (PrintWriter writer = new PrintWriter(jfo.openWriter())) {
        generate(writer);
      }
    } catch (IOException e) {
      messager.printMessage(Diagnostic.Kind.ERROR, "Failed to write shadow class file: " + e);
      throw new RuntimeException(e);
    }
  }

  void generate(PrintWriter writer) {
    writer.print("package " + shadowPackage + ";\n");
    for (String name : model.getImports()) {
      writer.println("import " + name + ';');
    }
    writer.println();
    writer.println("/**");
    writer.println(" * Shadow mapper. Automatically generated by the Robolectric Annotation Processor.");
    writer.println(" */");
    writer.println("@Generated(\"" + RobolectricProcessor.class.getCanonicalName() + "\")");
    if (priority != 0) {
      writer.println("@javax.annotation.Priority(" + priority + ")");
    }
    writer.println("@SuppressWarnings({\"unchecked\",\"deprecation\"})");
    writer.println("public class " + GEN_CLASS + " implements ShadowProvider {");

    writer.println("  private static final Map<String, String> SHADOW_MAP = new HashMap<>(" + (
        model.getAllShadowTypes().size() + model.getExtraShadowTypes().size()) + ");");
    writer.println();

    writer.println("  static {");
    for (ShadowInfo shadowInfo : model.getAllShadowTypes()) {
      final String shadow = shadowInfo.getShadowBinaryName();
      final String actual = shadowInfo.getActualName();
      if (shadowInfo.getShadowPickerBinaryName() == null) {
        writer.println("    SHADOW_MAP.put(\"" + actual + "\", \"" + shadow + "\");");
      }
    }

    for (Map.Entry<String, String> entry : model.getExtraShadowTypes().entrySet()) {
      final String shadow = entry.getKey();
      final String actual = entry.getValue();
      writer.println("    SHADOW_MAP.put(\"" + actual + "\", \"" + shadow + "\");");
    }

    writer.println("  }");
    writer.println();

    for (ShadowInfo shadowInfo : model.getVisibleShadowTypes()) {
      if (!shadowInfo.actualIsPublic()) {
        continue;
      }

      if (shadowInfo.getShadowPickerBinaryName() != null) {
        continue;
      }

      if (shadowInfo.shadowIsDeprecated()) {
        writer.println("  @Deprecated");
      }
      String paramDefStr = shadowInfo.getParamDefStr();
      final String shadow = shadowInfo.getShadowTypeWithParams();
      writer.println("  public static " + (paramDefStr.isEmpty() ? "" : paramDefStr + " ") + shadow
          + " shadowOf(" + shadowInfo.getActualTypeWithParams() + " actual) {");
      writer.println("    return (" + shadow + ") Shadow.extract(actual);");
      writer.println("  }");
      writer.println();
    }

    // this sucks, kill:
    for (Entry<String, ShadowInfo> entry : model.getShadowPickers().entrySet()) {
      ShadowInfo shadowInfo = entry.getValue();

      if (!shadowInfo.actualIsPublic() || !shadowInfo.isInAndroidSdk()) {
        continue;
      }

      if (shadowInfo.shadowIsDeprecated()) {
        writer.println("  @Deprecated");
      }
      String paramDefStr = shadowInfo.getParamDefStr();
      final String shadow = shadowInfo.getShadowName();
      writer.println("  public static " + (paramDefStr.isEmpty() ? "" : paramDefStr + " ") + shadow
          + " shadowOf(" + shadowInfo.getActualTypeWithParams() + " actual) {");
      writer.println("    return (" + shadow + ") Shadow.extract(actual);");
      writer.println("  }");
      writer.println();
    }

    writer.println("  @Override");
    writer.println("  public void reset() {");
    for (RobolectricModel.ResetterInfo resetterInfo : model.getResetters()) {
      int minSdk = resetterInfo.getMinSdk();
      int maxSdk = resetterInfo.getMaxSdk();
      String ifClause;
      if (minSdk != -1 && maxSdk != -1) {
        ifClause = "if (org.robolectric.RuntimeEnvironment.getApiLevel() >= " + minSdk
            + " && org.robolectric.RuntimeEnvironment.getApiLevel() <= " + maxSdk + ") ";
      } else if (maxSdk != -1) {
        ifClause = "if (org.robolectric.RuntimeEnvironment.getApiLevel() <= " + maxSdk + ") ";
      } else if (minSdk != -1) {
        ifClause = "if (org.robolectric.RuntimeEnvironment.getApiLevel() >= " + minSdk + ") ";
      } else {
        ifClause = "";
      }
      writer.println("    " + ifClause + resetterInfo.getMethodCall());
    }
    writer.println("  }");
    writer.println();

    writer.println("  @Override");
    writer.println("  public Map<String, String> getShadowMap() {");
    writer.println("    return SHADOW_MAP;");
    writer.println("  }");
    writer.println();

    writer.println("  @Override");
    writer.println("  public String[] getProvidedPackageNames() {");
    writer.println("    return new String[] {");
    if (shouldInstrumentPackages) {
      writer.println("      " + Joiner.on(",\n      ").join(model.getShadowedPackages()));
    }
    writer.println("    };");
    writer.println("  }");
    writer.println();

    TreeMap<String, ShadowInfo> shadowPickers = model.getShadowPickers();
    if (!shadowPickers.isEmpty()) {
      writer.println("  private static final Map<String, String> SHADOW_PICKER_MAP = " +
          "new HashMap<>(" + shadowPickers.size() + ");");
      writer.println();

      writer.println("  static {");
      for (Entry<String, ShadowInfo> entry : shadowPickers.entrySet()) {
        ShadowInfo shadowInfo = entry.getValue();
        final String actualBinaryName = shadowInfo.getActualBinaryName();
        final String shadowPickerClassName = shadowInfo.getShadowPickerBinaryName();
        writer.println("    SHADOW_PICKER_MAP.put(\"" + actualBinaryName + "\", " +
            "\"" + shadowPickerClassName + "\");");
      }
      writer.println("  }");
      writer.println();

      writer.println("  @Override");
      writer.println("  public Map<String, String> getShadowPickerMap() {");
      writer.println("    return SHADOW_PICKER_MAP;");
      writer.println("  }");
    }

    writer.println('}');
  }
}
